/**
 * http://surenpi.com
 */
package org.suren.autotest.web.framework.util;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @author suren
 * @date 2016年8月4日 下午12:31:31
 */
public class AnimatedGifEncoder
{

	protected int			width;							// image size
	protected int			height;
	protected Color			transparent	= null;			// transparent color
															// if given
	protected int			transIndex;					// transparent index
															// in color table
	protected int			repeat		= -1;				// no repeat
	protected int			delay		= 0;				// frame delay
															// (hundredths)
	protected boolean		started		= false;			// ready to output
															// frames
	protected OutputStream	out;
	protected BufferedImage	image;							// current frame
	protected byte[]		pixels;						// BGR byte array
															// from frame
	protected byte[]		indexedPixels;					// converted frame
															// indexed to
															// palette
	protected int			colorDepth;					// number of bit
															// planes
	protected byte[]		colorTab;						// RGB palette
	protected boolean[]		usedEntry	= new boolean[256]; // active palette
															// entries
	protected int			palSize		= 7;				// color table size
															// (bits-1)
	protected int			dispose		= -1;				// disposal code (-1
															// = use default)
	protected boolean		closeStream	= false;			// close stream when
															// finished
	protected boolean		firstFrame	= true;
	protected boolean		sizeSet		= false;			// if false, get
															// size from first
															// frame
	protected int			sample		= 10;				// default sample
															// interval for
															// quantizer

	/**
	 * Sets the delay time between each frame, or changes it for subsequent
	 * frames (applies to last frame added).
	 *
	 * @param ms
	 *            int delay time in milliseconds
	 */
	public void setDelay(int ms)
	{
		delay = Math.round(ms / 10.0f);
	}

	/**
	 * Sets the GIF frame disposal code for the last added frame and any
	 * subsequent frames. Default is 0 if no transparent color has been set,
	 * otherwise 2.
	 * 
	 * @param code
	 *            int disposal code.
	 */
	public void setDispose(int code)
	{
		if (code >= 0)
		{
			dispose = code;
		}
	}

	/**
	 * Sets the number of times the set of GIF frames should be played. Default
	 * is 1; 0 means play indefinitely. Must be invoked before the first image
	 * is added.
	 *
	 * @param iter
	 *            int number of iterations.
	 * @return
	 */
	public void setRepeat(int iter)
	{
		if (iter >= 0)
		{
			repeat = iter;
		}
	}

	/**
	 * Sets the transparent color for the last added frame and any subsequent
	 * frames. Since all colors are subject to modification in the quantization
	 * process, the color in the final palette for each frame closest to the
	 * given color becomes the transparent color for that frame. May be set to
	 * null to indicate no transparent color.
	 *
	 * @param c
	 *            Color to be treated as transparent on display.
	 */
	public void setTransparent(Color c)
	{
		transparent = c;
	}

	/**
	 * Adds next GIF frame. The frame is not written immediately, but is
	 * actually deferred until the next frame is received so that timing data
	 * can be inserted. Invoking <code>finish()</code> flushes all frames. If
	 * <code>setSize</code> was not invoked, the size of the first image is used
	 * for all subsequent frames.
	 *
	 * @param im
	 *            BufferedImage containing frame to write.
	 * @return true if successful.
	 */
	public boolean addFrame(BufferedImage im)
	{
		if ((im == null) || !started)
		{
			return false;
		}
		boolean ok = true;
		try
		{
			if (!sizeSet)
			{
				// use first frame's size
				setSize(im.getWidth(), im.getHeight());
			}
			image = im;
			getImagePixels(); // convert to correct format if necessary
			analyzePixels(); // build color table & map pixels
			if (firstFrame)
			{
				writeLSD(); // logical screen descriptior
				writePalette(); // global color table
				if (repeat >= 0)
				{
					// use NS app extension to indicate reps
					writeNetscapeExt();
				}
			}
			writeGraphicCtrlExt(); // write graphic control extension
			writeImageDesc(); // image descriptor
			if (!firstFrame)
			{
				writePalette(); // local color table
			}
			writePixels(); // encode and write pixel data
			firstFrame = false;
		}
		catch (IOException e)
		{
			ok = false;
		}
		return ok;
	}

	/**
	 * Flushes any pending data and closes output file. If writing to an
	 * OutputStream, the stream is not closed.
	 */
	public boolean finish()
	{
		if (!started)
			return false;
		boolean ok = true;
		started = false;
		try
		{
			out.write(0x3b); // gif trailer
			out.flush();
			if (closeStream)
			{
				out.close();
			}
		}
		catch (IOException e)
		{
			ok = false;
		}
		// reset for subsequent use
		transIndex = 0;
		out = null;
		image = null;
		pixels = null;
		indexedPixels = null;
		colorTab = null;
		closeStream = false;
		firstFrame = true;
		return ok;
	}

	/**
	 * Sets frame rate in frames per second. Equivalent to
	 * <code>setDelay(1000/fps)</code>.
	 *
	 * @param fps
	 *            float frame rate (frames per second)
	 */
	public void setFrameRate(float fps)
	{
		if (fps != 0f)
		{
			delay = Math.round(100f / fps);
		}
	}

	/**
	 * Sets quality of color quantization (conversion of images to the maximum
	 * 256 colors allowed by the GIF specification). Lower values (minimum = 1)
	 * produce better colors, but slow processing significantly. 10 is the
	 * default, and produces good color mapping at reasonable speeds. Values
	 * greater than 20 do not yield significant improvements in speed.
	 *
	 * @param quality
	 *            int greater than 0.
	 * @return
	 */
	public void setQuality(int quality)
	{
		if (quality < 1)
			quality = 1;
		sample = quality;
	}

	/**
	 * Sets the GIF frame size. The default size is the size of the first frame
	 * added if this method is not invoked.
	 *
	 * @param w
	 *            int frame width.
	 * @param h
	 *            int frame width.
	 */
	public void setSize(int w, int h)
	{
		if (started && !firstFrame)
			return;
		width = w;
		height = h;
		if (width < 1)
			width = 320;
		if (height < 1)
			height = 240;
		sizeSet = true;
	}

	/**
	 * Initiates GIF file creation on the given stream. The stream is not closed
	 * automatically.
	 *
	 * @param os
	 *            OutputStream on which GIF images are written.
	 * @return false if initial write failed.
	 */
	public boolean start(OutputStream os)
	{
		if (os == null)
			return false;
		boolean ok = true;
		closeStream = false;
		out = os;
		try
		{
			writeString("GIF89a"); // header
		}
		catch (IOException e)
		{
			ok = false;
		}
		return started = ok;
	}

	/**
	 * Initiates writing of a GIF file with the specified name.
	 *
	 * @param file
	 *            String containing output file name.
	 * @return false if open or initial write failed.
	 */
	public boolean start(String file)
	{
		boolean ok = true;
		try
		{
			out = new BufferedOutputStream(new FileOutputStream(file));
			ok = start(out);
			closeStream = true;
		}
		catch (IOException e)
		{
			ok = false;
		}
		return started = ok;
	}

	/**
	 * Analyzes image colors and creates color map.
	 */
	protected void analyzePixels()
	{
		int len = pixels.length;
		int nPix = len / 3;
		indexedPixels = new byte[nPix];
		NeuQuant nq = new NeuQuant(pixels, len, sample);
		// initialize quantizer
		colorTab = nq.process(); // create reduced palette
		// convert map from BGR to RGB
		for (int i = 0; i < colorTab.length; i += 3)
		{
			byte temp = colorTab[i];
			colorTab[i] = colorTab[i + 2];
			colorTab[i + 2] = temp;
			usedEntry[i / 3] = false;
		}
		// map image pixels to new palette
		int k = 0;
		for (int i = 0; i < nPix; i++)
		{
			int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff,
					pixels[k++] & 0xff);
			usedEntry[index] = true;
			indexedPixels[i] = (byte) index;
		}
		pixels = null;
		colorDepth = 8;
		palSize = 7;
		// get closest match to transparent color if specified
		if (transparent != null)
		{
			transIndex = findClosest(transparent);
		}
	}

	/**
	 * Returns index of palette color closest to c
	 *
	 */
	protected int findClosest(Color c)
	{
		if (colorTab == null)
			return -1;
		int r = c.getRed();
		int g = c.getGreen();
		int b = c.getBlue();
		int minpos = 0;
		int dmin = 256 * 256 * 256;
		int len = colorTab.length;
		for (int i = 0; i < len;)
		{
			int dr = r - (colorTab[i++] & 0xff);
			int dg = g - (colorTab[i++] & 0xff);
			int db = b - (colorTab[i] & 0xff);
			int d = dr * dr + dg * dg + db * db;
			int index = i / 3;
			if (usedEntry[index] && (d < dmin))
			{
				dmin = d;
				minpos = index;
			}
			i++;
		}
		return minpos;
	}

	/**
	 * Extracts image pixels into byte array "pixels"
	 */
	protected void getImagePixels()
	{
		int w = image.getWidth();
		int h = image.getHeight();
		int type = image.getType();
		if ((w != width) || (h != height)
				|| (type != BufferedImage.TYPE_3BYTE_BGR))
		{
			// create new image with right size/format
			BufferedImage temp = new BufferedImage(width, height,
					BufferedImage.TYPE_3BYTE_BGR);
			Graphics2D g = temp.createGraphics();
			g.drawImage(image, 0, 0, null);
			image = temp;
		}
		pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
	}

	/**
	 * Writes Graphic Control Extension
	 */
	protected void writeGraphicCtrlExt() throws IOException
	{
		out.write(0x21); // extension introducer
		out.write(0xf9); // GCE label
		out.write(4); // data block size
		int transp, disp;
		if (transparent == null)
		{
			transp = 0;
			disp = 0; // dispose = no action
		}
		else
		{
			transp = 1;
			disp = 2; // force clear if using transparent color
		}
		if (dispose >= 0)
		{
			disp = dispose & 7; // user override
		}
		disp <<= 2;
		// packed fields
		out.write(0 | // 1:3 reserved
				disp | // 4:6 disposal
				0 | // 7 user input - 0 = none
				transp); // 8 transparency flag
		writeShort(delay); // delay x 1/100 sec
		out.write(transIndex); // transparent color index
		out.write(0); // block terminator
	}

	/**
	 * Writes Image Descriptor
	 */
	protected void writeImageDesc() throws IOException
	{
		out.write(0x2c); // image separator
		writeShort(0); // image position x,y = 0,0
		writeShort(0);
		writeShort(width); // image size
		writeShort(height);
		// packed fields
		if (firstFrame)
		{
			// no LCT - GCT is used for first (or only) frame
			out.write(0);
		}
		else
		{
			// specify normal LCT
			out.write(0x80 | // 1 local color table 1=yes
					0 | // 2 interlace - 0=no
					0 | // 3 sorted - 0=no
					0 | // 4-5 reserved
					palSize); // 6-8 size of color table
		}
	}

	/**
	 * Writes Logical Screen Descriptor
	 */
	protected void writeLSD() throws IOException
	{
		// logical screen size
		writeShort(width);
		writeShort(height);
		// packed fields
		out.write((0x80 | // 1 : global color table flag = 1 (gct used)
		0x70 | // 2-4 : color resolution = 7
		0x00 | // 5 : gct sort flag = 0
		palSize)); // 6-8 : gct size
		out.write(0); // background color index
		out.write(0); // pixel aspect ratio - assume 1:1
	}

	/**
	 * Writes Netscape application extension to define repeat count.
	 */
	protected void writeNetscapeExt() throws IOException
	{
		out.write(0x21); // extension introducer
		out.write(0xff); // app extension label
		out.write(11); // block size
		writeString("NETSCAPE" + "2.0"); // app id + auth code
		out.write(3); // sub-block size
		out.write(1); // loop sub-block id
		writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
		out.write(0); // block terminator
	}

	/**
	 * Writes color table
	 */
	protected void writePalette() throws IOException
	{
		out.write(colorTab, 0, colorTab.length);
		int n = (3 * 256) - colorTab.length;
		for (int i = 0; i < n; i++)
		{
			out.write(0);
		}
	}

	/**
	 * Encodes and writes pixel data
	 */
	protected void writePixels() throws IOException
	{
		LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels,
				colorDepth);
		encoder.encode(out);
	}

	/**
	 * Write 16-bit value to output stream, LSB first
	 */
	protected void writeShort(int value) throws IOException
	{
		out.write(value & 0xff);
		out.write((value >> 8) & 0xff);
	}

	/**
	 * Writes string to output stream
	 */
	protected void writeString(String s) throws IOException
	{
		for (int i = 0; i < s.length(); i++)
		{
			out.write((byte) s.charAt(i));
		}
	}
}
